package com.kbtd.utils.redis;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/**
 * @author wp
 * @version 1.0
 */
@Component
public class JsonRedisTemplate extends RedisTemplate<String, Object> {
    private static final String REDIS_NULL_VALUE = "";
    private static final Object REDIS_LOCK_VALUE = "";
    private static final Integer THREAD_POOL_SIZE = 10;
    private static final ExecutorService executor = Executors.
            newFixedThreadPool(THREAD_POOL_SIZE);

    public JsonRedisTemplate(RedisConnectionFactory factory, ObjectMapper objectMapper) {
        super.setConnectionFactory(factory);
        objectMapper.activateDefaultTyping
                (objectMapper.getPolymorphicTypeValidator(),
                ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.PROPERTY);

        GenericJackson2JsonRedisSerializer jsonRedisSerializer =
                new GenericJackson2JsonRedisSerializer(objectMapper);

        super.setKeySerializer(RedisSerializer.string());
        super.setValueSerializer(jsonRedisSerializer);
        super.setHashKeySerializer(RedisSerializer.string());
        super.setHashValueSerializer(jsonRedisSerializer);
    }


    /**
     * 添加缓存
     * 逻辑过期解决缓存击穿问题
     * @param keyPrefix 键前缀
     * @param id 键id
     * @param v 值
     * @param expireTime 过期时间
     * @param timeUnit 时间单位
     */
    public void setWithLogicalExpire(String keyPrefix, Long id,
        @NonNull Object v, long expireTime, TimeUnit timeUnit) {
        if (!StringUtils.hasLength(keyPrefix)) {
            keyPrefix = "";
        }
        String k = keyPrefix + id;
        RedisData redisData = new RedisData();
        redisData.setData(v);
        if (timeUnit == null) {
            timeUnit = TimeUnit.MILLISECONDS;
        }
        long expire = timeUnit.toMillis(expireTime) + System.currentTimeMillis();
        Instant instant = Instant.ofEpochSecond(expire);
        redisData.setExpireTime(LocalDateTime.ofInstant
                (instant, ZoneId.systemDefault()));

        opsForValue().set(k, redisData);
    }

    public void setWithLogicalExpire(String keyPrefix, Long id,
                                     @NonNull Object v, long l) {
        setWithLogicalExpire(keyPrefix, id, v, l, null);
    }

    /**
     * 获取缓存值
     * 解决缓存穿透问题
     * @param keyPrefix 键前缀
     * @param id 键id
     * @param sqlQueryFunction sql查询函数
     * @return 值
     */
    public <R> R getWithBreakdown(String keyPrefix, Long id,
        @NonNull Function<Long, R> sqlQueryFunction, long expireTime, TimeUnit timeUnit) {
        if (!StringUtils.hasLength(keyPrefix)) {
            keyPrefix = "";
        }
        if (timeUnit == null) {
            timeUnit = TimeUnit.MILLISECONDS;
        }
        String key = keyPrefix + id;

        R value =  (R) opsForValue().get(key);

        if (REDIS_NULL_VALUE.equals(value)) {
            //redis查询存储为null情况
            return null;
        } else if (ObjectUtils.isEmpty(value)) {
            //redis未查询到情况
            R result = sqlQueryFunction.apply(id);
            if (result == null) {
                opsForValue().set(key, REDIS_NULL_VALUE, expireTime, timeUnit);
            } else {
                opsForValue().set(key, result, expireTime, timeUnit);
            }
            return result;
        }
        return value;
    }

    /**
     * 获取缓存
     * 逻辑过期解决缓存击穿
     * @param keyPrefix 键前缀
     * @param id 键id
     * @param sqlQueryFunction sql查询函数
     * @param expireTime 过期时间
     * @param timeUnit 时间单位
     * @param <R> 值类型
     * @return 值
     */
    public <R> R getWithLogicExpire(String keyPrefix, Long id,
        @NonNull Function<Long, R> sqlQueryFunction,
        long expireTime, TimeUnit timeUnit) {
        if (!StringUtils.hasLength(keyPrefix)) {
            keyPrefix = "";
        }
        String key = keyPrefix + id;
        Object value = opsForValue().get(key);
        if (value instanceof RedisData) {
            RedisData redisData = (RedisData) value;
            if (redisData.isExpire() && getLock(keyPrefix, id)) {
                //过期
                String copyKeyPrefix = keyPrefix;
                Thread thread = new Thread(() -> {
                    R updatedValue = sqlQueryFunction.apply(id);
                    setWithLogicalExpire(copyKeyPrefix, id, updatedValue, expireTime, timeUnit);
                    removeLock(copyKeyPrefix, id);
                });
                executor.submit(thread);
            }
            return (R) redisData.getData();
        } else if (ObjectUtils.isEmpty(value)) {
            return null;
        }

        throw new ClassCastException("缓存值类" + value.getClass() +
                "不为" + RedisData.class.getName() + ", " + "即获取缓存非逻辑过期缓存");

    }

    /**
     * 获取缓存穿透锁
     * @param keyLockPrefix 键锁前缀
     * @param id 键id
     * @return 是否成功
     */
    private boolean getLock(String keyLockPrefix, Long id) {
        if (!StringUtils.hasLength(keyLockPrefix)) {
            keyLockPrefix = "";
        }
        String key = keyLockPrefix + id;
        Boolean flag = opsForValue().setIfAbsent(key, REDIS_LOCK_VALUE);
        return Boolean.TRUE.equals(flag);
    }

    /**
     * 释放缓存穿透锁
     * @param keyLockPrefix 键锁前缀
     * @param id 键id
     */
    private void removeLock(String keyLockPrefix, Long id) {
        if (!StringUtils.hasLength(keyLockPrefix)) {
            keyLockPrefix = "";
        }
        String key = keyLockPrefix + id;
        delete(key);
    }




    @Data
    public static class RedisData {
        private LocalDateTime expireTime;
        private Object data;

        public boolean isExpire() {
            return expireTime.isBefore(LocalDateTime.now());
        }

        public <T> T getData(Class<T> canvasType) {
            Class<?> dataClass = this.data.getClass();
            if (canvasType.isAssignableFrom(dataClass)) {
                return canvasType.cast(this.getData());
            }
            throw new RuntimeException("Class not from " +
                    dataClass.getName() + " to " + canvasType.getName());
        }
    }
}
